Finish implementing `cargo install`
authorAlex Crichton <alex@alexcrichton.com>
Mon, 5 Oct 2015 22:29:15 +0000 (15:29 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Mon, 19 Oct 2015 04:44:01 +0000 (21:44 -0700)
This commit is an implementation of [RFC 1200][rfc] which brings two new
subcommands: `cargo install` and `cargo uninstall`. Most of this is a straight
implementation of the RFC, but a few tweaks were made:

* The `-p` or `--package` arguments are no longer needed as you just pass
  `crate` as a bare argument to the command, this means `cargo install foo`
  works and downloads from crates.io by default.
* Some logic around selecting which crate in a multi-crate repo is installed has
  been tweaked slightly, but mostly in the realm of "let's do the thing that
  makes sense" rather than the literal "let's do what's in the RFC".
  Specifically, we don't pick a crate with examples if there are multiple crates
  with binaries (instead an error is generated saying there are multiple binary
  crates).

[rfc]: https://github.com/rust-lang/rfcs/blob/master/text/1200-cargo-install.md

21 files changed:
src/bin/cargo.rs
src/bin/install.rs
src/bin/uninstall.rs [new file with mode: 0644]
src/cargo/core/package_id_spec.rs
src/cargo/core/registry.rs
src/cargo/core/resolver/mod.rs
src/cargo/core/source.rs
src/cargo/ops/cargo_compile.rs
src/cargo/ops/cargo_install.rs
src/cargo/ops/cargo_package.rs
src/cargo/ops/cargo_pkgid.rs
src/cargo/ops/mod.rs
src/cargo/ops/resolve.rs
src/cargo/sources/git/source.rs
src/cargo/sources/path.rs
src/cargo/sources/registry.rs
src/cargo/util/config.rs
tests/support/mod.rs
tests/support/registry.rs
tests/test_cargo_install.rs [new file with mode: 0644]
tests/tests.rs

index 11019d1ead464f744bdb9369e05b637f4e5888c4..f21ffb1d5c52d040758cd4127886a7e6d812b6f1 100644 (file)
@@ -82,6 +82,7 @@ macro_rules! each_subcommand{ ($mac:ident) => ({
     $mac!(rustc);
     $mac!(search);
     $mac!(test);
+    $mac!(uninstall);
     $mac!(update);
     $mac!(verify_project);
     $mac!(version);
index 8f443341b840c7d4bdc13b9b9d015128b7a960b2..7ae2a130aff0bd6c94830c004554c8b35c42654d 100644 (file)
@@ -1,8 +1,8 @@
-use cargo::ops;
-use cargo::util::{CliResult, CliError, Config};
 use std::path::Path;
 
-#[allow(dead_code)] // for now until all options are implemented
+use cargo::ops;
+use cargo::core::{SourceId, GitReference};
+use cargo::util::{CliResult, Config, ToUrl, human};
 
 #[derive(RustcDecodable)]
 struct Options {
@@ -10,48 +10,65 @@ struct Options {
     flag_features: Vec<String>,
     flag_no_default_features: bool,
     flag_debug: bool,
-    flag_bin: Option<String>,
+    flag_bin: Vec<String>,
     flag_example: Vec<String>,
-    flag_package: Vec<String>,
     flag_verbose: bool,
+    flag_quiet: bool,
+    flag_color: Option<String>,
     flag_root: Option<String>,
+    flag_list: bool,
+
+    arg_crate: Option<String>,
+    flag_vers: Option<String>,
+
+    flag_git: Option<String>,
+    flag_branch: Option<String>,
+    flag_tag: Option<String>,
+    flag_rev: Option<String>,
+
+    flag_path: Option<String>,
 }
 
 pub const USAGE: &'static str = "
-Install a crate onto the local system
+Install a Rust binary
 
-Installing new crates:
-    cargo install [options]
-    cargo install [options] [-p CRATE | --package CRATE] [--vers VERS]
-    cargo install [options] --git URL [--branch BRANCH | --tag TAG | --rev SHA]
-    cargo install [options] --path PATH
-
-Managing installed crates:
+Usage:
+    cargo install [options] [<crate>]
     cargo install [options] --list
 
-Options:
-    -h, --help              Print this message
-    -j N, --jobs N          The number of jobs to run in parallel
-    --features FEATURES     Space-separated list of features to activate
-    --no-default-features   Do not build the `default` feature
-    --debug                 Build in debug mode instead of release mode
-    --bin NAME              Only install the binary NAME
-    --example EXAMPLE       Install the example EXAMPLE instead of binaries
-    -p, --package CRATE     Install this crate from crates.io or select the
-                            package in a repository/path to install.
-    -v, --verbose           Use verbose output
-    --root DIR              Directory to install packages into
+Specifying what crate to install:
+    --vers VERS               Specify a version to install from crates.io
+    --git URL                 Git URL to install the specified crate from
+    --branch BRANCH           Branch to use when installing from git
+    --tag TAG                 Tag to use when installing from git
+    --rev SHA                 Specific commit to use when installing from git
+    --path PATH               Filesystem path to local crate to install
+
+Build and install options:
+    -h, --help                Print this message
+    -j N, --jobs N            The number of jobs to run in parallel
+    --features FEATURES       Space-separated list of features to activate
+    --no-default-features     Do not build the `default` feature
+    --debug                   Build in debug mode instead of release mode
+    --bin NAME                Only install the binary NAME
+    --example EXAMPLE         Install the example EXAMPLE instead of binaries
+    --root DIR                Directory to install packages into
+    -v, --verbose             Use verbose output
+    -q, --quiet               Less output printed to stdout
+    --color WHEN              Coloring: auto, always, never
 
 This command manages Cargo's local set of install binary crates. Only packages
 which have [[bin]] targets can be installed, and all binaries are installed into
-`$HOME/.cargo/bin` by default (or `$CARGO_HOME/bin` if you change the home
-directory).
+the installation root's `bin` folder. The installation root is determined, in
+order of precedence, by `--root`, `$CARGO_INSTALL_ROOT`, the `install.root`
+configuration key, and finally the home directory (which is either
+`$CARGO_HOME` if set or `$HOME/.cargo` by default).
 
-There are multiple methods of installing a new crate onto the system. The
-`cargo install` command with no arguments will install the current crate (as
-specifed by the current directory). Otherwise the `-p`, `--package`, `--git`,
-and `--path` options all specify the source from which a crate is being
-installed. The `-p` and `--package` options will download crates from crates.io.
+There are multiple sources from which a crate can be installed. The default
+location is crates.io but the `--git` and `--path` flags can change this source.
+If the source contains more than one package (such as crates.io or a git
+repository with multiple crates) the `<crate>` argument is required to indicate
+which crate should be installed.
 
 Crates from crates.io can optionally specify the version they wish to install
 via the `--vers` flags, and similarly packages from git repositories can
@@ -64,7 +81,8 @@ The `--list` option will list all installed packages (and their versions).
 ";
 
 pub fn execute(options: Options, config: &Config) -> CliResult<Option<()>> {
-    config.shell().set_verbose(options.flag_verbose);
+    try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet));
+    try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..])));
 
     let compile_opts = ops::CompileOptions {
         config: config,
@@ -72,18 +90,41 @@ pub fn execute(options: Options, config: &Config) -> CliResult<Option<()>> {
         target: None,
         features: &options.flag_features,
         no_default_features: options.flag_no_default_features,
-        spec: None,
+        spec: &[],
         exec_engine: None,
         mode: ops::CompileMode::Build,
-        release: true,
-        filter: ops::CompileFilter::Everything,
+        release: !options.flag_debug,
+        filter: ops::CompileFilter::new(false, &options.flag_bin, &[],
+                                        &options.flag_example, &[]),
         target_rustc_args: None,
     };
 
-    let root = &Path::new("$HOME/.cargo/bin");
+    let source = if let Some(url) = options.flag_git {
+        let url = try!(url.to_url().map_err(human));
+        let gitref = if let Some(branch) = options.flag_branch {
+            GitReference::Branch(branch)
+        } else if let Some(tag) = options.flag_tag {
+            GitReference::Tag(tag)
+        } else if let Some(rev) = options.flag_rev {
+            GitReference::Rev(rev)
+        } else {
+            GitReference::Branch("master".to_string())
+        };
+        SourceId::for_git(&url, gitref)
+    } else if let Some(path) = options.flag_path {
+        try!(SourceId::for_path(Path::new(&path)))
+    } else {
+        try!(SourceId::for_central(config))
+    };
+
+    let krate = options.arg_crate.as_ref().map(|s| &s[..]);
+    let vers = options.flag_vers.as_ref().map(|s| &s[..]);
+    let root = options.flag_root.as_ref().map(|s| &s[..]);
 
-    ops::install(&root,
-                 &compile_opts).map_err(|err| {
-        CliError::from_boxed(err, 101)
-    }).map(|_| None)
+    if options.flag_list {
+        try!(ops::install_list(root, config));
+    } else {
+        try!(ops::install(root, krate, &source, vers, &compile_opts));
+    }
+    Ok(None)
 }
diff --git a/src/bin/uninstall.rs b/src/bin/uninstall.rs
new file mode 100644 (file)
index 0000000..20d4b57
--- /dev/null
@@ -0,0 +1,43 @@
+use cargo::ops;
+use cargo::util::{CliResult, Config};
+
+#[derive(RustcDecodable)]
+struct Options {
+    flag_bin: Vec<String>,
+    flag_root: Option<String>,
+    flag_verbose: bool,
+    flag_quiet: bool,
+    flag_color: Option<String>,
+
+    arg_spec: String,
+}
+
+pub const USAGE: &'static str = "
+Remove a Rust binary
+
+Usage:
+    cargo uninstall [options] <spec>
+
+Options:
+    -h, --help                Print this message
+    --root DIR                Directory to uninstall packages from
+    --bin NAME                Only uninstall the binary NAME
+    -v, --verbose             Use verbose output
+    -q, --quiet               Less output printed to stdout
+    --color WHEN              Coloring: auto, always, never
+
+The argument SPEC is a package id specification (see `cargo help pkgid`) to
+specify which crate should be uninstalled. By default all binaries are
+uninstalled for a crate but the `--bin` and `--example` flags can be used to
+only uninstall particular binaries.
+";
+
+pub fn execute(options: Options, config: &Config) -> CliResult<Option<()>> {
+    try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet));
+    try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..])));
+
+    let root = options.flag_root.as_ref().map(|s| &s[..]);
+    try!(ops::uninstall(root, &options.arg_spec, &options.flag_bin, config));
+    Ok(None)
+}
+
index a950e6006bc66ec7a1f6f20d9e53a233429cbabc..15b191e96974ea17cd7c2672d10e121331115efd 100644 (file)
@@ -1,4 +1,6 @@
+use std::collections::HashMap;
 use std::fmt;
+
 use semver::Version;
 use url::{self, Url, UrlParser};
 
@@ -45,6 +47,15 @@ impl PackageIdSpec {
         })
     }
 
+    pub fn query_str<'a, I>(spec: &str, i: I) -> CargoResult<&'a PackageId>
+        where I: IntoIterator<Item=&'a PackageId>
+    {
+        let spec = try!(PackageIdSpec::parse(spec).chain_error(|| {
+            human(format!("invalid package id specification: `{}`", spec))
+        }));
+        spec.query(i)
+    }
+
     pub fn from_package_id(package_id: &PackageId) -> PackageIdSpec {
         PackageIdSpec {
             name: package_id.name().to_string(),
@@ -115,6 +126,51 @@ impl PackageIdSpec {
             None => true
         }
     }
+
+    pub fn query<'a, I>(&self, i: I) -> CargoResult<&'a PackageId>
+        where I: IntoIterator<Item=&'a PackageId>
+    {
+        let mut ids = i.into_iter().filter(|p| self.matches(*p));
+        let ret = match ids.next() {
+            Some(id) => id,
+            None => return Err(human(format!("package id specification `{}` \
+                                              matched no packages", self))),
+        };
+        return match ids.next() {
+            Some(other) => {
+                let mut msg = format!("There are multiple `{}` packages in \
+                                       your project, and the specification \
+                                       `{}` is ambiguous.\n\
+                                       Please re-run this command \
+                                       with `-p <spec>` where `<spec>` is one \
+                                       of the following:",
+                                      self.name(), self);
+                let mut vec = vec![ret, other];
+                vec.extend(ids);
+                minimize(&mut msg, vec, self);
+                Err(human(msg))
+            }
+            None => Ok(ret)
+        };
+
+        fn minimize(msg: &mut String,
+                    ids: Vec<&PackageId>,
+                    spec: &PackageIdSpec) {
+            let mut version_cnt = HashMap::new();
+            for id in ids.iter() {
+                *version_cnt.entry(id.version()).or_insert(0) += 1;
+            }
+            for id in ids.iter() {
+                if version_cnt[id.version()] == 1 {
+                    msg.push_str(&format!("\n  {}:{}", spec.name(),
+                                          id.version()));
+                } else {
+                    msg.push_str(&format!("\n  {}",
+                                          PackageIdSpec::from_package_id(*id)));
+                }
+            }
+        }
+    }
 }
 
 fn url(s: &str) -> url::ParseResult<Url> {
index 1c21e0d960716cdec7f19e141d399fb5b55fc35b..037c5d4174e51bd9a81ff26f5734463e8e1879c8 100644 (file)
@@ -154,6 +154,16 @@ impl<'cfg> PackageRegistry<'cfg> {
         Ok(())
     }
 
+    pub fn add_preloaded(&mut self, id: &SourceId, source: Box<Source + 'cfg>) {
+        self.add_source(id, source, Kind::Locked);
+    }
+
+    fn add_source(&mut self, id: &SourceId, source: Box<Source + 'cfg>,
+                  kind: Kind) {
+        self.sources.insert(id, source);
+        self.source_ids.insert(id.clone(), (id.clone(), kind));
+    }
+
     pub fn add_overrides(&mut self, ids: Vec<SourceId>) -> CargoResult<()> {
         for id in ids.iter() {
             try!(self.load(id, Kind::Override));
@@ -183,8 +193,7 @@ impl<'cfg> PackageRegistry<'cfg> {
             }
 
             // Save off the source
-            self.sources.insert(source_id, source);
-            self.source_ids.insert(source_id.clone(), (source_id.clone(), kind));
+            self.add_source(source_id, source, kind);
 
             Ok(())
         }).chain_error(|| human(format!("Unable to update {}", source_id)))
index bd9ba548ac9ffc171722f247a5ec5da48d872c0c..bfade99626944f4451f6e6037caa6920d30a9ff5 100644 (file)
@@ -55,7 +55,7 @@ use semver;
 
 use core::{PackageId, Registry, SourceId, Summary, Dependency};
 use core::PackageIdSpec;
-use util::{CargoResult, Graph, human, ChainError, CargoError};
+use util::{CargoResult, Graph, human, CargoError};
 use util::profile;
 use util::graph::{Nodes, Edges};
 
@@ -118,55 +118,13 @@ impl Resolve {
         self.graph.edges(pkg)
     }
 
-    pub fn query(&self, spec: &str) -> CargoResult<&PackageId> {
-        let spec = try!(PackageIdSpec::parse(spec).chain_error(|| {
-            human(format!("invalid package id specification: `{}`", spec))
-        }));
-        let mut ids = self.iter().filter(|p| spec.matches(*p));
-        let ret = match ids.next() {
-            Some(id) => id,
-            None => return Err(human(format!("package id specification `{}` \
-                                              matched no packages", spec))),
-        };
-        return match ids.next() {
-            Some(other) => {
-                let mut msg = format!("There are multiple `{}` packages in \
-                                       your project, and the specification \
-                                       `{}` is ambiguous.\n\
-                                       Please re-run this command \
-                                       with `-p <spec>` where `<spec>` is one \
-                                       of the following:",
-                                      spec.name(), spec);
-                let mut vec = vec![ret, other];
-                vec.extend(ids);
-                minimize(&mut msg, vec, &spec);
-                Err(human(msg))
-            }
-            None => Ok(ret)
-        };
-
-        fn minimize(msg: &mut String,
-                    ids: Vec<&PackageId>,
-                    spec: &PackageIdSpec) {
-            let mut version_cnt = HashMap::new();
-            for id in ids.iter() {
-                *version_cnt.entry(id.version()).or_insert(0) += 1;
-            }
-            for id in ids.iter() {
-                if version_cnt[id.version()] == 1 {
-                    msg.push_str(&format!("\n  {}:{}", spec.name(),
-                                          id.version()));
-                } else {
-                    msg.push_str(&format!("\n  {}",
-                                          PackageIdSpec::from_package_id(*id)));
-                }
-            }
-        }
-    }
-
     pub fn features(&self, pkg: &PackageId) -> Option<&HashSet<String>> {
         self.features.get(pkg)
     }
+
+    pub fn query(&self, spec: &str) -> CargoResult<&PackageId> {
+        PackageIdSpec::query_str(spec, self.iter())
+    }
 }
 
 impl fmt::Debug for Resolve {
index d18010a36ef7cc69378427cbbc166b276ecb64d1..d258b9f160f0a8ff79342a8a1b593d0c7ce35bb6 100644 (file)
@@ -129,17 +129,19 @@ impl SourceId {
                 SourceId::new(Kind::Registry, url)
                          .with_precise(Some("locked".to_string()))
             }
-            "path" => SourceId::for_path(Path::new(&url[5..])).unwrap(),
+            "path" => {
+                let url = url.to_url().unwrap();
+                SourceId::new(Kind::Path, url)
+            }
             _ => panic!("Unsupported serialized SourceId")
         }
     }
 
     pub fn to_url(&self) -> String {
         match *self.inner {
-            SourceIdInner { kind: Kind::Path, .. } => {
-                panic!("Path sources are not included in the lockfile, \
-                       so this is unimplemented")
-            },
+            SourceIdInner { kind: Kind::Path, ref url, .. } => {
+                format!("path+{}", url)
+            }
             SourceIdInner {
                 kind: Kind::Git(ref reference), ref url, ref precise, ..
             } => {
index b52cae00213df17fdcf2ebdeb6be0c00f5735428..e9263a69d1b983722a482842bcc399fd55a67436 100644 (file)
@@ -92,11 +92,12 @@ pub fn compile<'a>(manifest_path: &Path,
     for key in package.manifest().warnings().iter() {
         try!(options.config.shell().warn(key))
     }
-    compile_pkg(&package, options)
+    compile_pkg(&package, None, options)
 }
 
 #[allow(deprecated)] // connect => join in 1.3
 pub fn compile_pkg<'a>(root_package: &Package,
+                       source: Option<Box<Source + 'a>>,
                        options: &CompileOptions<'a>)
                        -> CargoResult<ops::Compilation<'a>> {
     let CompileOptions { config, jobs, target, spec, features,
@@ -122,6 +123,10 @@ pub fn compile_pkg<'a>(root_package: &Package,
     let (packages, resolve_with_overrides, sources) = {
         let mut registry = PackageRegistry::new(options.config);
 
+        if let Some(source) = source {
+            registry.add_preloaded(root_package.package_id().source_id(), source);
+        }
+
         // First, resolve the root_package's *listed* dependencies, as well as
         // downloading and updating all remotes and such.
         let resolve = try!(ops::resolve_pkg(&mut registry, root_package));
index 2bd65cc0dd6330a7af566d4d877b54870747962e..4d1f8370a91360f114fb775b5e9bf1ff10fbe789 100644 (file)
-use ops;
-use util::CargoResult;
-use sources::PathSource;
-use std::path::Path;
+use std::collections::btree_map::Entry;
+use std::collections::{BTreeMap, BTreeSet};
+use std::env;
+use std::ffi::OsString;
+use std::fs::{self, File};
+use std::io::prelude::*;
+use std::path::{Path, PathBuf};
 
-pub fn install(manifest_path: &Path,
+use toml;
+
+use core::{SourceId, Source, Package, Registry, Dependency, PackageIdSpec};
+use core::PackageId;
+use ops::{self, CompileFilter};
+use sources::{GitSource, PathSource, RegistrySource};
+use util::{CargoResult, ChainError, Config, human, internal};
+
+#[derive(RustcDecodable, RustcEncodable)]
+enum CrateListing {
+    V1(CrateListingV1),
+}
+
+#[derive(RustcDecodable, RustcEncodable)]
+struct CrateListingV1 {
+    v1: BTreeMap<PackageId, BTreeSet<String>>,
+}
+
+struct Transaction {
+    bins: Vec<PathBuf>,
+}
+
+impl Drop for Transaction {
+    fn drop(&mut self) {
+        for bin in self.bins.iter() {
+            let _ = fs::remove_file(bin);
+        }
+    }
+}
+
+pub fn install(root: Option<&str>,
+               krate: Option<&str>,
+               source_id: &SourceId,
+               vers: Option<&str>,
                opts: &ops::CompileOptions) -> CargoResult<()> {
     let config = opts.config;
-    let src = try!(PathSource::for_path(manifest_path.parent().unwrap(),
-                                            config));
-    let _root = try!(src.root_package());
+    let root = try!(resolve_root(root, config));
+    let (pkg, source) = if source_id.is_git() {
+        try!(select_pkg(GitSource::new(source_id, config), source_id,
+                        krate, vers, &mut |git| git.read_packages()))
+    } else if source_id.is_path() {
+        let path = source_id.url().to_file_path().ok()
+                            .expect("path sources must have a valid path");
+        try!(select_pkg(PathSource::new(&path, source_id, config),
+                        source_id, krate, vers,
+                        &mut |path| path.read_packages()))
+    } else {
+        try!(select_pkg(RegistrySource::new(source_id, config),
+                        source_id, krate, vers,
+                        &mut |_| Err(human("must specify a crate to install from \
+                                            crates.io"))))
+    };
+
+    let mut list = try!(read_crate_list(&root));
+    let dst = root.join("bin");
+    try!(check_overwrites(&dst, &pkg, &opts.filter, &list));
+
+    let target_dir = config.cwd().join("target-install");
+    config.set_target_dir(&target_dir);
+    let compile = try!(ops::compile_pkg(&pkg, Some(source), opts).chain_error(|| {
+        human(format!("failed to compile `{}`, intermediate artifacts can be \
+                       found at `{}`", pkg, target_dir.display()))
+    }));
+
+    let mut t = Transaction { bins: Vec::new() };
+    try!(fs::create_dir_all(&dst));
+    for bin in compile.binaries.iter() {
+        let dst = dst.join(bin.file_name().unwrap());
+        try!(config.shell().status("Installing", dst.display()));
+        try!(fs::copy(&bin, &dst).chain_error(|| {
+            human(format!("failed to copy `{}` to `{}`", bin.display(),
+                          dst.display()))
+        }));
+        t.bins.push(dst);
+    }
+    try!(fs::remove_dir_all(&target_dir));
+
+    list.v1.entry(pkg.package_id().clone()).or_insert_with(|| {
+        BTreeSet::new()
+    }).extend(t.bins.iter().map(|t| {
+        t.file_name().unwrap().to_string_lossy().into_owned()
+    }));
+    try!(write_crate_list(&root, list));
+
+    t.bins.truncate(0);
+
+    // Print a warning that if this directory isn't in PATH that they won't be
+    // able to run these commands.
+    let path = env::var_os("PATH").unwrap_or(OsString::new());
+    for path in env::split_paths(&path) {
+        if path == dst {
+            return Ok(())
+        }
+    }
+
+    try!(config.shell().warn(&format!("be sure to add `{}` to your PATH to be \
+                                       able to run the installed binaries",
+                                      dst.display())));
+    Ok(())
+}
+
+fn select_pkg<'a, T>(mut source: T,
+                     source_id: &SourceId,
+                     name: Option<&str>,
+                     vers: Option<&str>,
+                     list_all: &mut FnMut(&mut T) -> CargoResult<Vec<Package>>)
+                     -> CargoResult<(Package, Box<Source + 'a>)>
+    where T: Source + 'a
+{
+    try!(source.update());
+    match name {
+        Some(name) => {
+            let dep = try!(Dependency::parse(name, vers, source_id));
+            let deps = try!(source.query(&dep));
+            match deps.iter().map(|p| p.package_id()).max() {
+                Some(pkgid) => {
+                    try!(source.download(&[pkgid.clone()]));
+                    Ok((try!(source.get(&[pkgid.clone()])).remove(0),
+                        Box::new(source)))
+                }
+                None => {
+                    let vers_info = vers.map(|v| format!(" with version `{}`", v))
+                                        .unwrap_or(String::new());
+                    Err(human(format!("could not find `{}` in `{}`{}", name,
+                                      source_id, vers_info)))
+                }
+            }
+        }
+        None => {
+            let candidates = try!(list_all(&mut source));
+            let binaries = candidates.iter().filter(|cand| {
+                cand.targets().iter().filter(|t| t.is_bin()).count() > 0
+            });
+            let examples = candidates.iter().filter(|cand| {
+                cand.targets().iter().filter(|t| t.is_example()).count() > 0
+            });
+            let pkg = match try!(one(binaries, |v| multi_err("binaries", v))) {
+                Some(p) => p,
+                None => {
+                    match try!(one(examples, |v| multi_err("examples", v))) {
+                        Some(p) => p,
+                        None => return Err(human("no packages found with \
+                                                  binaries or examples")),
+                    }
+                }
+            };
+            return Ok((pkg.clone(), Box::new(source)));
 
-    println!("Compiling");
-    try!(ops::compile(manifest_path, opts));
+            #[allow(deprecated)] // connect => join in 1.3
+            fn multi_err(kind: &str, mut pkgs: Vec<&Package>) -> String {
+                pkgs.sort_by(|a, b| a.name().cmp(b.name()));
+                format!("multiple packages with {} found: {}", kind,
+                        pkgs.iter().map(|p| p.name()).collect::<Vec<_>>()
+                            .connect(", "))
+            }
+        }
+    }
+}
+
+fn one<I, F>(mut i: I, f: F) -> CargoResult<Option<I::Item>>
+    where I: Iterator,
+          F: FnOnce(Vec<I::Item>) -> String
+{
+    match (i.next(), i.next()) {
+        (Some(i1), Some(i2)) => {
+            let mut v = vec![i1, i2];
+            v.extend(i);
+            Err(human(f(v)))
+        }
+        (Some(i), None) => Ok(Some(i)),
+        (None, _) => Ok(None)
+    }
+}
+
+fn check_overwrites(dst: &Path,
+                    pkg: &Package,
+                    filter: &ops::CompileFilter,
+                    prev: &CrateListingV1) -> CargoResult<()> {
+    let check = |name| {
+        let name = format!("{}{}", name, env::consts::EXE_SUFFIX);
+        if fs::metadata(dst.join(&name)).is_err() {
+            return Ok(())
+        }
+        let mut msg = format!("binary `{}` already exists in destination", name);
+        if let Some((p, _)) = prev.v1.iter().find(|&(_, v)| v.contains(&name)) {
+            msg.push_str(&format!(" as part of `{}`", p));
+        }
+        Err(human(msg))
+    };
+    match *filter {
+        CompileFilter::Everything => {
+            // If explicit --bin or --example flags were passed then those'll
+            // get checked during cargo_compile, we only care about the "build
+            // everything" case here
+            if pkg.targets().iter().filter(|t| t.is_bin()).next().is_none() {
+                return Err(human("specified package has no binaries"))
+            }
+
+            for target in pkg.targets().iter().filter(|t| t.is_bin()) {
+                try!(check(target.name()));
+            }
+        }
+        CompileFilter::Only { bins, examples, .. } => {
+            for bin in bins.iter().chain(examples) {
+                try!(check(bin));
+            }
+        }
+    }
+    Ok(())
+}
+
+fn read_crate_list(path: &Path) -> CargoResult<CrateListingV1> {
+    let metadata = path.join(".crates.toml");
+    let mut f = match File::open(&metadata) {
+        Ok(f) => f,
+        Err(..) => return Ok(CrateListingV1 { v1: BTreeMap::new() }),
+    };
+    (|| -> CargoResult<_> {
+        let mut contents = String::new();
+        try!(f.read_to_string(&mut contents));
+        let listing = try!(toml::decode_str(&contents).chain_error(|| {
+            internal("invalid TOML found for metadata")
+        }));
+        match listing {
+            CrateListing::V1(v1) => Ok(v1),
+        }
+    }).chain_error(|| {
+        human(format!("failed to parse crate metadata at `{}`",
+                      metadata.display()))
+    })
+}
+
+fn write_crate_list(path: &Path, listing: CrateListingV1) -> CargoResult<()> {
+    let metadata = path.join(".crates.toml");
+    (|| -> CargoResult<_> {
+        let mut f = try!(File::create(&metadata));
+        let data = toml::encode_str::<CrateListing>(&CrateListing::V1(listing));
+        try!(f.write_all(data.as_bytes()));
+        Ok(())
+    }).chain_error(|| {
+        human(format!("failed to write crate metadata at `{}`",
+                      metadata.display()))
+    })
+}
+
+pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
+    let dst = try!(resolve_root(dst, config));
+    let list = try!(read_crate_list(&dst));
+    let mut shell = config.shell();
+    let out = shell.out();
+    for (k, v) in list.v1.iter() {
+        try!(writeln!(out, "{}:", k));
+        for bin in v {
+            try!(writeln!(out, "    {}", bin));
+        }
+    }
+    Ok(())
+}
+
+pub fn uninstall(root: Option<&str>,
+                 spec: &str,
+                 bins: &[String],
+                 config: &Config) -> CargoResult<()> {
+    let root = try!(resolve_root(root, config));
+    let mut metadata = try!(read_crate_list(&root));
+    let mut to_remove = Vec::new();
+    {
+        let result = try!(PackageIdSpec::query_str(spec, metadata.v1.keys()))
+                                        .clone();
+        let mut installed = match metadata.v1.entry(result.clone()) {
+            Entry::Occupied(e) => e,
+            Entry::Vacant(..) => panic!("entry not found: {}", result),
+        };
+        let dst = root.join("bin");
+        for bin in installed.get() {
+            let bin = dst.join(bin);
+            if fs::metadata(&bin).is_err() {
+                return Err(human(format!("corrupt metadata, `{}` does not \
+                                          exist when it should",
+                                          bin.display())))
+            }
+        }
+
+        let bins = bins.iter().map(|s| {
+            if s.ends_with(env::consts::EXE_SUFFIX) {
+                s.to_string()
+            } else {
+                format!("{}{}", s, env::consts::EXE_SUFFIX)
+            }
+        }).collect::<Vec<_>>();
+
+        for bin in bins.iter() {
+            if !installed.get().contains(bin) {
+                return Err(human(format!("binary `{}` not installed as part \
+                                          of `{}`", bin, result)))
+            }
+        }
+
+        if bins.len() == 0 {
+            to_remove.extend(installed.get().iter().map(|b| dst.join(b)));
+            installed.get_mut().clear();
+        } else {
+            for bin in bins.iter() {
+                to_remove.push(dst.join(bin));
+                installed.get_mut().remove(bin);
+            }
+        }
+        if installed.get().len() == 0 {
+            installed.remove();
+        }
+    }
+    try!(write_crate_list(&root, metadata));
+    for bin in to_remove {
+        try!(config.shell().status("Removing", bin.display()));
+        try!(fs::remove_file(bin));
+    }
 
     Ok(())
 }
+
+fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult<PathBuf> {
+    let config_root = try!(config.get_string("install.root"));
+    Ok(flag.map(PathBuf::from).or_else(|| {
+        env::var_os("CARGO_INSTALL_ROOT").map(PathBuf::from)
+    }).or_else(|| {
+        config_root.clone().map(|(v, _)| PathBuf::from(v))
+    }).unwrap_or_else(|| {
+        config.home().to_owned()
+    }))
+}
index a1b3cd5afd5d4700599ebd0ae1c489628078e6e8..e08281814b197dc8f39cf93bd10311319af23ed0 100644 (file)
@@ -208,7 +208,7 @@ fn run_verify(config: &Config, pkg: &Package, tar: &Path)
     let new_pkg = Package::new(new_manifest, &manifest_path);
 
     // Now that we've rewritten all our path dependencies, compile it!
-    try!(ops::compile_pkg(&new_pkg, &ops::CompileOptions {
+    try!(ops::compile_pkg(&new_pkg, None, &ops::CompileOptions {
         config: config,
         jobs: None,
         target: None,
index ab5cfe6911ddc6d7a333dbd408003e195df41002..48fad309ec00c4cbe742adc1b366c2baca56685b 100644 (file)
@@ -17,7 +17,7 @@ pub fn pkgid(manifest_path: &Path,
     };
 
     let pkgid = match spec {
-        Some(spec) => try!(resolve.query(spec)),
+        Some(spec) => try!(PackageIdSpec::query_str(spec, resolve.iter())),
         None => package.package_id(),
     };
     Ok(PackageIdSpec::from_package_id(pkgid))
index 5aa156cd61f21006f8f64dbf51808b2aba3ce740..92141afb4393a0e7d0e39fc0fea938235dbb7e86 100644 (file)
@@ -7,7 +7,7 @@ pub use self::cargo_rustc::{Context, LayoutProxy};
 pub use self::cargo_rustc::{BuildOutput, BuildConfig, TargetConfig};
 pub use self::cargo_rustc::{CommandType, CommandPrototype, ExecEngine, ProcessEngine};
 pub use self::cargo_run::run;
-pub use self::cargo_install::install;
+pub use self::cargo_install::{install, install_list, uninstall};
 pub use self::cargo_new::{new, NewOptions, VersionControl};
 pub use self::cargo_doc::{doc, DocOptions};
 pub use self::cargo_generate_lockfile::{generate_lockfile};
index e7afbcbca07538dc7aadaf29934457e848bb2aad..f1dbb0f8154b3a616d95dbca5398274b44c12d50 100644 (file)
@@ -17,7 +17,9 @@ pub fn resolve_pkg(registry: &mut PackageRegistry, package: &Package)
     let resolve = try!(resolve_with_previous(registry, package,
                                              Method::Everything,
                                              prev.as_ref(), None));
-    try!(ops::write_pkg_lockfile(package, &resolve));
+    if package.package_id().source_id().is_path() {
+        try!(ops::write_pkg_lockfile(package, &resolve));
+    }
     Ok(resolve)
 }
 
index 1863e8fd237b3a958ceace48fd8a1decc590b5a5..eaf66adccfb0739e506558e1f0e462c7360a3e96 100644 (file)
@@ -67,6 +67,13 @@ impl<'cfg> GitSource<'cfg> {
     }
 
     pub fn url(&self) -> &Url { self.remote.url() }
+
+    pub fn read_packages(&mut self) -> CargoResult<Vec<Package>> {
+        if self.path_source.is_none() {
+            try!(self.update());
+        }
+        self.path_source.as_mut().unwrap().read_packages()
+    }
 }
 
 fn ident(url: &Url) -> String {
index 1da040f1a1be1e4b4341f953e06f84735316371e..eedb88303bf107440c50ad99abb6727dd911a1ea 100644 (file)
@@ -56,7 +56,7 @@ impl<'cfg> PathSource<'cfg> {
         }
     }
 
-    fn read_packages(&self) -> CargoResult<Vec<Package>> {
+    pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
         if self.updated {
             Ok(self.packages.clone())
         } else if self.id.is_path() && self.id.precise().is_some() {
index 3d0c67dca7f1a00fe8b1d6cdad0c59997ca9a1c2..49d9cf20796a278856b269949aea4980e584e2f4 100644 (file)
@@ -187,7 +187,7 @@ pub struct RegistrySource<'cfg> {
     src_path: PathBuf,
     config: &'cfg Config,
     handle: Option<http::Handle>,
-    sources: Vec<PathSource<'cfg>>,
+    sources: HashMap<PackageId, PathSource<'cfg>>,
     hashes: HashMap<(String, String), String>, // (name, vers) => cksum
     cache: HashMap<String, Vec<(Summary, bool)>>,
     updated: bool,
@@ -239,7 +239,7 @@ impl<'cfg> RegistrySource<'cfg> {
             config: config,
             source_id: source_id.clone(),
             handle: None,
-            sources: Vec::new(),
+            sources: HashMap::new(),
             hashes: HashMap::new(),
             cache: HashMap::new(),
             updated: false,
@@ -366,7 +366,7 @@ impl<'cfg> RegistrySource<'cfg> {
     }
 
     /// Parse the on-disk metadata for the package provided
-    fn summaries(&mut self, name: &str) -> CargoResult<&Vec<(Summary, bool)>> {
+    pub fn summaries(&mut self, name: &str) -> CargoResult<&Vec<(Summary, bool)>> {
         if self.cache.contains_key(name) {
             return Ok(self.cache.get(name).unwrap());
         }
@@ -537,6 +537,7 @@ impl<'cfg> Source for RegistrySource<'cfg> {
         let url = try!(config.dl.to_url().map_err(internal));
         for package in packages.iter() {
             if self.source_id != *package.source_id() { continue }
+            if self.sources.contains_key(package) { continue }
 
             let mut url = url.clone();
             url.path_mut().unwrap().push(package.name().to_string());
@@ -551,14 +552,14 @@ impl<'cfg> Source for RegistrySource<'cfg> {
             }));
             let mut src = PathSource::new(&path, &self.source_id, self.config);
             try!(src.update());
-            self.sources.push(src);
+            self.sources.insert(package.clone(), src);
         }
         Ok(())
     }
 
     fn get(&self, packages: &[PackageId]) -> CargoResult<Vec<Package>> {
         let mut ret = Vec::new();
-        for src in self.sources.iter() {
+        for src in self.sources.values() {
             ret.extend(try!(src.get(packages)).into_iter());
         }
         return Ok(ret);
index 21aa840b7eb68c0053d3584c29a3d112e9418a6a..de5302f6fdda397cd5b1766df37360ca8acdf0aa 100644 (file)
@@ -27,7 +27,7 @@ pub struct Config {
     cwd: PathBuf,
     rustc: PathBuf,
     rustdoc: PathBuf,
-    target_dir: Option<PathBuf>,
+    target_dir: RefCell<Option<PathBuf>>,
 }
 
 impl Config {
@@ -48,7 +48,7 @@ impl Config {
             values_loaded: Cell::new(false),
             rustc: PathBuf::from("rustc"),
             rustdoc: PathBuf::from("rustdoc"),
-            target_dir: None,
+            target_dir: RefCell::new(None),
         };
 
         try!(cfg.scrape_tool_config());
@@ -101,11 +101,15 @@ impl Config {
     pub fn cwd(&self) -> &Path { &self.cwd }
 
     pub fn target_dir(&self, pkg: &Package) -> PathBuf {
-        self.target_dir.clone().unwrap_or_else(|| {
+        self.target_dir.borrow().clone().unwrap_or_else(|| {
             pkg.root().join("target")
         })
     }
 
+    pub fn set_target_dir(&self, path: &Path) {
+        *self.target_dir.borrow_mut() = Some(path.to_owned());
+    }
+
     pub fn get(&self, key: &str) -> CargoResult<Option<ConfigValue>> {
         let vals = try!(self.values());
         let mut parts = key.split('.').enumerate();
@@ -237,9 +241,9 @@ impl Config {
             path.pop();
             path.pop();
             path.push(dir);
-            self.target_dir = Some(path);
+            *self.target_dir.borrow_mut() = Some(path);
         } else if let Some(dir) = env::var_os("CARGO_TARGET_DIR") {
-            self.target_dir = Some(self.cwd.join(dir));
+            *self.target_dir.borrow_mut() = Some(self.cwd.join(dir));
         }
         Ok(())
     }
index 90b1dfac81f30d7c73ed230c542936c5a5dc7454..b345f245033bb51ce59f3f0546f355da4a3a5e3c 100644 (file)
@@ -563,4 +563,4 @@ pub static DOWNLOADING: &'static str = " Downloading";
 pub static UPLOADING:   &'static str = "   Uploading";
 pub static VERIFYING:   &'static str = "   Verifying";
 pub static ARCHIVING:   &'static str = "   Archiving";
-pub static INSTALLED:   &'static str = "   Installed";
+pub static INSTALLING:  &'static str = "  Installing";
index 861bc5a11dcd5d1548b47ed420b1cbb441046048..be48cb7845ec94d89ce4947d334a23a8b59b6e9a 100644 (file)
@@ -55,7 +55,11 @@ pub fn mock_archive(name: &str, version: &str, deps: &[(&str, &str, &str)]) {
     }
     let p = project(name)
         .file("Cargo.toml", &manifest)
-        .file("src/lib.rs", "");
+        .file("src/lib.rs", "")
+        .file("src/main.rs", &format!("\
+            extern crate {};
+            fn main() {{}}
+        ", name));
     p.build();
 
     let dst = mock_archive_dst(name, version);
@@ -66,6 +70,8 @@ pub fn mock_archive(name: &str, version: &str, deps: &[(&str, &str, &str)]) {
                   &mut File::open(&p.root().join("Cargo.toml")).unwrap()).unwrap();
     a.append_file(&format!("{}-{}/src/lib.rs", name, version),
                   &mut File::open(&p.root().join("src/lib.rs")).unwrap()).unwrap();
+    a.append_file(&format!("{}-{}/src/main.rs", name, version),
+                  &mut File::open(&p.root().join("src/main.rs")).unwrap()).unwrap();
     a.finish().unwrap();
 }
 
diff --git a/tests/test_cargo_install.rs b/tests/test_cargo_install.rs
new file mode 100644 (file)
index 0000000..c283579
--- /dev/null
@@ -0,0 +1,500 @@
+use std::fmt;
+use std::fs::{self, File};
+use std::io::prelude::*;
+use std::path::{Path, PathBuf};
+
+use cargo::util::{process, ProcessBuilder};
+use hamcrest::{assert_that, existing_file, is_not, Matcher, MatchResult};
+
+use support::{project, execs, cargo_dir};
+use support::{UPDATING, DOWNLOADING, COMPILING, INSTALLING, REMOVING};
+use support::paths;
+use support::registry as r;
+use support::git;
+
+use self::InstalledExe as has_installed_exe;
+
+fn setup() {
+    r::init();
+}
+
+fn cargo_process(s: &str) -> ProcessBuilder {
+    let mut p = process(&cargo_dir().join("cargo")).unwrap();
+    p.arg(s).cwd(&paths::root())
+     .env("HOME", &paths::home())
+     .env_remove("CARGO_HOME");
+    return p;
+}
+
+fn exe(name: &str) -> String {
+    if cfg!(windows) {format!("{}.exe", name)} else {name.to_string()}
+}
+
+fn cargo_home() -> PathBuf {
+    paths::home().join(".cargo")
+}
+
+struct InstalledExe(&'static str);
+
+impl<P: AsRef<Path>> Matcher<P> for InstalledExe {
+    fn matches(&self, path: P) -> MatchResult {
+        let path = path.as_ref().join("bin").join(exe(self.0));
+        existing_file().matches(&path)
+    }
+}
+
+impl fmt::Display for InstalledExe {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "installed exe `{}`", self.0)
+    }
+}
+
+test!(simple {
+    r::mock_pkg("foo", "0.0.1", &[]);
+
+    assert_that(cargo_process("install").arg("foo"),
+                execs().with_status(0).with_stdout(&format!("\
+{updating} registry `[..]`
+{downloading} foo v0.0.1 (registry file://[..])
+{compiling} foo v0.0.1 (registry file://[..])
+{installing} {home}[..]bin[..]foo[..]
+",
+        updating = UPDATING,
+        downloading = DOWNLOADING,
+        compiling = COMPILING,
+        installing = INSTALLING,
+        home = cargo_home().display())));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+
+    assert_that(cargo_process("uninstall").arg("foo"),
+                execs().with_status(0).with_stdout(&format!("\
+{removing} {home}[..]bin[..]foo[..]
+",
+        removing = REMOVING,
+        home = cargo_home().display())));
+    assert_that(cargo_home(), is_not(has_installed_exe("foo")));
+});
+
+test!(pick_max_version {
+    r::mock_pkg("foo", "0.0.1", &[]);
+    r::mock_pkg("foo", "0.0.2", &[]);
+
+    assert_that(cargo_process("install").arg("foo"),
+                execs().with_status(0).with_stdout(&format!("\
+{updating} registry `[..]`
+{downloading} foo v0.0.2 (registry file://[..])
+{compiling} foo v0.0.2 (registry file://[..])
+{installing} {home}[..]bin[..]foo[..]
+",
+        updating = UPDATING,
+        downloading = DOWNLOADING,
+        compiling = COMPILING,
+        installing = INSTALLING,
+        home = cargo_home().display())));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+});
+
+test!(missing {
+    r::mock_pkg("foo", "0.0.1", &[]);
+    assert_that(cargo_process("install").arg("bar"),
+                execs().with_status(101).with_stderr("\
+could not find `bar` in `registry file://[..]`
+"));
+});
+
+test!(bad_version {
+    r::mock_pkg("foo", "0.0.1", &[]);
+    assert_that(cargo_process("install").arg("foo").arg("--vers=0.2.0"),
+                execs().with_status(101).with_stderr("\
+could not find `foo` in `registry file://[..]` with version `0.2.0`
+"));
+});
+
+test!(no_crate {
+    assert_that(cargo_process("install"),
+                execs().with_status(101).with_stderr("\
+must specify a crate to install from crates.io
+"));
+});
+
+test!(install_location_precedence {
+    r::mock_pkg("foo", "0.0.1", &[]);
+
+    let root = paths::root();
+    let t1 = root.join("t1");
+    let t2 = root.join("t2");
+    let t3 = root.join("t3");
+    let t4 = cargo_home();
+
+    fs::create_dir(root.join(".cargo")).unwrap();
+    File::create(root.join(".cargo/config")).unwrap().write_all(format!("\
+        [install]
+        root = '{}'
+    ", t3.display()).as_bytes()).unwrap();
+
+    println!("install --root");
+
+    assert_that(cargo_process("install").arg("foo")
+                            .arg("--root").arg(&t1)
+                            .env("CARGO_INSTALL_ROOT", &t2),
+                execs().with_status(0));
+    assert_that(&t1, has_installed_exe("foo"));
+    assert_that(&t2, is_not(has_installed_exe("foo")));
+
+    println!("install CARGO_INSTALL_ROOT");
+
+    assert_that(cargo_process("install").arg("foo")
+                            .env("CARGO_INSTALL_ROOT", &t2),
+                execs().with_status(0));
+    assert_that(&t2, has_installed_exe("foo"));
+    assert_that(&t3, is_not(has_installed_exe("foo")));
+
+    println!("install install.root");
+
+    assert_that(cargo_process("install").arg("foo"),
+                execs().with_status(0));
+    assert_that(&t3, has_installed_exe("foo"));
+    assert_that(&t4, is_not(has_installed_exe("foo")));
+
+    fs::remove_file(root.join(".cargo/config")).unwrap();
+
+    println!("install cargo home");
+
+    assert_that(cargo_process("install").arg("foo"),
+                execs().with_status(0));
+    assert_that(&t4, has_installed_exe("foo"));
+});
+
+test!(install_path {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/main.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+});
+
+test!(multiple_crates_error {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/main.rs", "fn main() {}")
+        .file("a/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("a/src/main.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(101).with_stderr("\
+multiple packages with binaries found: bar, foo
+"));
+});
+
+test!(multiple_crates_select {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/main.rs", "fn main() {}")
+        .file("a/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("a/src/main.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()).arg("foo"),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+    assert_that(cargo_home(), is_not(has_installed_exe("bar")));
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()).arg("bar"),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("bar"));
+});
+
+test!(multiple_crates_auto_binaries {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+
+            [dependencies]
+            bar = { path = "a" }
+        "#)
+        .file("src/main.rs", "extern crate bar; fn main() {}")
+        .file("a/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("a/src/lib.rs", "");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+});
+
+test!(multiple_crates_auto_examples {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+
+            [dependencies]
+            bar = { path = "a" }
+        "#)
+        .file("src/lib.rs", "extern crate bar;")
+        .file("examples/foo.rs", "
+            extern crate bar;
+            extern crate foo;
+            fn main() {}
+        ")
+        .file("a/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("a/src/lib.rs", "");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root())
+                                        .arg("--example=foo"),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+});
+
+test!(no_binaries_or_examples {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+
+            [dependencies]
+            bar = { path = "a" }
+        "#)
+        .file("src/lib.rs", "")
+        .file("a/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("a/src/lib.rs", "");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(101).with_stderr("\
+no packages found with binaries or examples
+"));
+});
+
+test!(no_binaries {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/lib.rs", "")
+        .file("examples/foo.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()).arg("foo"),
+                execs().with_status(101).with_stderr("\
+specified package has no binaries
+"));
+});
+
+test!(examples {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/lib.rs", "")
+        .file("examples/foo.rs", "extern crate foo; fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root())
+                                        .arg("--example=foo"),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+});
+
+test!(install_twice {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/main.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(0));
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(101).with_stderr("\
+binary `foo[..]` already exists in destination as part of `foo v0.1.0 ([..])`
+"));
+});
+
+test!(compile_failure {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/main.rs", "");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(101).with_stderr("\
+error: main function not found
+error: aborting due to previous error
+failed to compile `foo v0.1.0 (file://[..])`, intermediate artifacts can be \
+    found at `[..]target-install`
+
+Caused by:
+  Could not compile `foo`.
+
+To learn more, run the command again with --verbose.
+"));
+});
+
+test!(git_repo {
+    let p = git::repo(&paths::root().join("foo"))
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/main.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--git").arg(p.url().to_string()),
+                execs().with_status(0).with_stdout(&format!("\
+{updating} git repository `[..]`
+{compiling} foo v0.1.0 ([..])
+{installing} {home}[..]bin[..]foo[..]
+",
+        updating = UPDATING,
+        compiling = COMPILING,
+        installing = INSTALLING,
+        home = cargo_home().display())));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+});
+
+test!(list {
+    r::mock_pkg("foo", "0.0.1", &[]);
+    r::mock_pkg("bar", "0.2.1", &[]);
+    r::mock_pkg("bar", "0.2.2", &[]);
+
+    assert_that(cargo_process("install").arg("--list"),
+                execs().with_status(0).with_stdout(""));
+
+    assert_that(cargo_process("install").arg("bar").arg("--vers").arg("=0.2.1"),
+                execs().with_status(0));
+    assert_that(cargo_process("install").arg("foo"),
+                execs().with_status(0));
+    assert_that(cargo_process("install").arg("--list"),
+                execs().with_status(0).with_stdout("\
+bar v0.2.1 (registry [..]):
+    bar[..]
+foo v0.0.1 (registry [..]):
+    foo[..]
+"));
+});
+
+test!(uninstall_pkg_does_not_exist {
+    assert_that(cargo_process("uninstall").arg("foo"),
+                execs().with_status(101).with_stderr("\
+package id specification `foo` matched no packages
+"));
+});
+
+test!(uninstall_bin_does_not_exist {
+    r::mock_pkg("foo", "0.0.1", &[]);
+
+    assert_that(cargo_process("install").arg("foo"),
+                execs().with_status(0));
+    assert_that(cargo_process("uninstall").arg("foo").arg("--bin=bar"),
+                execs().with_status(101).with_stderr("\
+binary `bar[..]` not installed as part of `foo v0.0.1 ([..])`
+"));
+});
+
+test!(uninstall_piecemeal {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/bin/foo.rs", "fn main() {}")
+        .file("src/bin/bar.rs", "fn main() {}");
+    p.build();
+
+    assert_that(cargo_process("install").arg("--path").arg(p.root()),
+                execs().with_status(0));
+    assert_that(cargo_home(), has_installed_exe("foo"));
+    assert_that(cargo_home(), has_installed_exe("bar"));
+
+    assert_that(cargo_process("uninstall").arg("foo").arg("--bin=bar"),
+                execs().with_status(0).with_stdout(&format!("\
+{removing} [..]bar[..]
+", removing = REMOVING)));
+
+    assert_that(cargo_home(), has_installed_exe("foo"));
+    assert_that(cargo_home(), is_not(has_installed_exe("bar")));
+
+    assert_that(cargo_process("uninstall").arg("foo").arg("--bin=foo"),
+                execs().with_status(0).with_stdout(&format!("\
+{removing} [..]foo[..]
+", removing = REMOVING)));
+    assert_that(cargo_home(), is_not(has_installed_exe("foo")));
+
+    assert_that(cargo_process("uninstall").arg("foo"),
+                execs().with_status(101).with_stderr("\
+package id specification `foo` matched no packages
+"));
+});
index ad6d9abb8833878d94962d614e57b0ad39b817c9..612d11e0344b886374322d6e20373d1077205bfc 100644 (file)
@@ -48,6 +48,7 @@ mod test_cargo_features;
 mod test_cargo_fetch;
 mod test_cargo_freshness;
 mod test_cargo_generate_lockfile;
+mod test_cargo_install;
 mod test_cargo_new;
 mod test_cargo_package;
 mod test_cargo_profiles;